/*
 * Copyright (C) 2020 Jimmy Hogoboom
 *
 * This file is part of Limitlist.
 *
 * Limitlist is free software: you can redistribute it and/or modify
 * it under the terms of the Affero GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Limitlist is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Affero GNU General Public License for more details.
 *
 * You should have received a copy of the Affero GNU General Public License
 * along with Limitlist.  If not, see <https://www.gnu.org/licenses/>.
 */

// SPDX-License-Identifier: AGPL-3.0-or-later

import React, { PropsWithChildren, ReactNode, ReactElement, useState, useEffect, useCallback } from "react";
import uuid from 'uuid';
import { IStorage } from "storage";
import { IProductListItem } from "core/types/IProductListItem";
import { arrayMove } from "utils";

export interface IProductListManagerProps {
  listId: string,
  storage: IStorage,
}

export interface IProductListManagedComponentProps {
  items?: IProductListItem[],
  editingItemIds?: string[],
  addItem?: () => IProductListItem,
  updateItem?: (item: IProductListItem) => void,
  deleteItem?: (item: IProductListItem) => void,
  moveItem?: (oldIndex: number, newIndex: number) => void,
}

const ProductListManager: React.FC<PropsWithChildren<IProductListManagerProps>> = (props) => {
  const [items, setItems] = useState([] as IProductListItem[]);
  const [editingItemIds, setEditingItemIds] = useState([] as string[]);
  const [isLoaded, setIsLoaded] = useState(false);

  // Use callback to only hit storage when listId changes (see below)
  const onListIdChange = useCallback(() => {
    const fetchList = async () => {
      const items = await props.storage.Lists.find(props.listId) || [];

      setItems(items);

      setIsLoaded(true);
    };
    fetchList();
  }, [props.listId, props.storage.Lists]);
  useEffect(onListIdChange, [props.listId]);

  // Create a callback that updates when any of its dependencies change.
  // This allows us to avoid updating storage when any prop other than items changes.
  // When the props update, this only updates the callback. Then the callback can be used
  // in a useEffect block when different dependencies change (like the items).
  const onItemsChange = useCallback(() => {
    // Don't try to update storage until after the list has been first loaded.
    // Without this, the effect is trigged immediately upon mount, which results in
    // updating the store with an empty array.
    if (!isLoaded) return;

    props.storage.Lists.set(props.listId, items);
  }, [isLoaded, items, props.listId, props.storage.Lists]);

  // Update the list in storage whenever it changes.
  useEffect(onItemsChange, [items]);

  const addItem = () => {
    // TODO: Something else should probably be in charge of manufacturing new instances of items
    const newItem = {
      id: uuid(),
      name: '',
      price: null,
      quantity: null,
    } as IProductListItem;

    setItems(items => [
      ...items,
      newItem,
    ]);

    setEditingItemIds((ids) => ([...ids, newItem.id]));
  };

  const deleteItem = (item: IProductListItem) => {
    setItems(items => items.filter(i => item.id !== i.id));

    // Remove item from editing ids if it gets deleted to avoid an ever-expanding list of ids.
    if (editingItemIds.includes(item.id)) {
      setEditingItemIds(ids => ids.filter(id => id !== item.id));
    }
  };

  const updateItem = (item: IProductListItem) => {
    setItems(items => items.map(i => item.id === i.id
      ? { ...item }
      : i));

    // Remove item from editing ids if it gets updated to avoid an ever-expanding list of ids.
    if (editingItemIds.includes(item.id)) {
      setEditingItemIds(ids => ids.filter(id => id !== item.id));
    }
  };

  const moveItem = (oldIndex: number, newIndex: number) => {
    setItems(items => arrayMove(items, oldIndex, newIndex));
  };

  // Create clones of the children with additional props
  const childrenWithProps = React.Children.map(props.children, (child: ReactNode) =>
    React.cloneElement(child as ReactElement, { items, editingItemIds, addItem, deleteItem, updateItem, moveItem })
  );
  return (
    <>
      {childrenWithProps}
    </>
  );
};

export default ProductListManager;
